<?php

namespace app\model;

use app\model\common\BaseModel;
use AlibabaCloud\Client\AlibabaCloud;
use AlibabaCloud\Client\Exception\ClientException;
use AlibabaCloud\Client\Exception\ServerException;
use Carbon\Carbon;
use Carbon\CarbonImmutable;
use think\exception\ValidateException;

/**
 * 短信验证码模型
 * 
 * 功能说明：
 * 
 * 支持多场景使用，每个场景之间逻辑独立
 * - 重发限制，同场景内同手机号在上一条限制时间之前无法再次重发（不管是否使用）
 * - 有效期，每一条验证码有独立的有效期，仅在有效期内有效，与重发限制时间无关
 * 
 * 
-- ----------------------------
-- Table structure for sms_code
-- ----------------------------
DROP TABLE IF EXISTS `sms_code`;
CREATE TABLE `sms_code`  (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `scene` tinyint(1) NOT NULL DEFAULT 0 COMMENT '场景值（模型字典）',
  `mobile` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '手机号',
  `is_used` tinyint(1) NOT NULL DEFAULT 0 COMMENT '是否使用',
  `code` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '验证码',
  `expire_time` datetime NULL DEFAULT NULL COMMENT '过期时间',
  `retry_time` datetime NULL DEFAULT NULL COMMENT '重发解锁时间',
  `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 5 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '短信_验证码' ROW_FORMAT = Dynamic;

 */

/**
 * 短信验证码
 */
class SmsCode extends BaseModel
{
	public static $modelAlias = '短信验证码';

	protected $table = 'sms_code';
	protected $pk = 'id';

	/** 字典 */
	// 使用场景
	const SCENE_MAP = [
		['id' => 1, 'name' => '用户注册登录']
	];

	/**
	 * 实现具体发送（需要自己实现）
	 * 内部读取配置项，实例化SDK，调用短信API
	 * @throws Exception
	 */
	public static function realSend($mobile, $code)
	{
		// ... 调用，如果失败抛出异常即可
		// 执行发送
		$config = config('aliyun');
		AlibabaCloud::accessKeyClient($config['accessKeyId'], $config['accessKeySecret'])
			->regionId($config['sms']['regionId'])
			->asDefaultClient();

		$send = AlibabaCloud::rpc()
			->product('Dysmsapi')
			// ->scheme('https') // https | http
			->version('2017-05-25')
			->action('SendSms')
			->method('POST')
			->host('dysmsapi.aliyuncs.com')
			->options([
				'query' => [
					// 'RegionId' => $smsConfig['region_id'],
					'PhoneNumbers' => $mobile,
					'SignName' => $config['sms']['sign'],
					'TemplateCode' => $config['sms']['templates']['login_code'],
					'TemplateParam' => json_encode(['code' => $code]),
				],
			])
			->request()->toArray();
		if ($send['Code'] != 'OK') {
			throw new \Exception($send['Message'] ?? '短信发送失败，未知异常');
		}

		// 发送成功
		// "Message" => "OK"
		// "RequestId" => "9C0F2A02-5BF6-5885-B5B6-AD7F419B4CE8"
		// "Code" => "OK"
		// "BizId" => "100422962454158097^0"

		// 失败
		// "Message" => "触发小时级流控Permits:5"
		// "RequestId" => "EEA35579-AA8A-5238-9618-41B019ECF7DF"
		// "Code" => "isv.BUSINESS_LIMIT_CONTROL"
	}

	/**
	 * 发送验证码
	 * @param int $scene 场景
	 * @param string $mobile 手机号
	 * @param int $retry 重发限制时间（秒）
	 * @param int $expire 有效期（秒）
	 * @throws ValidateException|Exception
	 */
	public static function send($scene, $mobile, $code_len = 6, $retry = 60, $expire = 180)
	{
		validate(['scene' => $scene, 'mobile' => $mobile], [
			'scene' => 'require|in:' . implode(',', array_column(static::SCENE_MAP, 'id')),
			'mobile' => 'require|mobile'
		]);

		$now = CarbonImmutable::now();

		// 检查重发限制
		$checkRetry = static::where([
			'scene' => $scene,
			'mobile' => $mobile,
		])
			->whereTime('retry_time', '>=', $now->format('Y-m-d H:i:s'))
			->order('id', 'DESC')->find();

		if ($checkRetry) throw new ValidateException('发送过于频繁，请稍后再试');

		// 执行发送
		$code = static::createCode($code_len);
		static::realSend($mobile, $code);
		return static::create([
			'scene' => $scene,
			'mobile' => $mobile,
			'code' => $code,
			'expire_time' => $now->addSeconds($expire),
			'retry_time' => $now->addSeconds($retry)
		]);
	}

	/**
	 * 检查验证码
	 * @param string $scene
	 * @param string $mobile
	 * @param string $code
	 * @return bool
	 */
	public static function checkCode($scene, $mobile, $code)
	{
		$find = static::where('scene', $scene)
			->where('mobile', $mobile)
			->where('code', $code)
			->where('is_used', 0)
			->whereTime('expire_time', '>=', time())
			->find();
		if (!$find) return false;
		$find->save(['is_used' => 1]);
		return true;
	}

	/**
	 * 生成验证码code
	 * @param integer $len 长度
	 * @return void
	 */
	protected static function createCode($len = 4)
	{
		$str = '';
		while (strlen($str) < $len) {
			$str .= rand(0, 9);
		}
		return $str;
	}
}
